package javax.generic.persistance;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import javax.generic.swing.ClassUtils;
import javax.generic.swing.FieldInfo;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.TypedQuery;
import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Path;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;

import org.apache.log4j.Logger;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;

/**
 * This class provides default implementations for the {@link GenericDao}
 * methods.
 * 
 * @author mdinic
 * @param <T> type of the persisted entity this DAO will handle
 */
@SuppressWarnings({ "rawtypes", "unchecked" })
@Repository
public class GenericDaoImpl implements GenericDao {

	private static final Logger LOG = Logger.getLogger(GenericDaoImpl.class);

	@PersistenceContext
	private EntityManager entityManager;

	@Override
	@Transactional
	public final void persist(AbstractPersistableEntity entity) {
		if (entity.getGodina() == null) {
			entity.setGodina(Calendar.getInstance().get(Calendar.YEAR));
		}
		entityManager.persist(entity);
		entityManager.flush();
	}

	@Override
	@Transactional
	public final AbstractPersistableEntity update(AbstractPersistableEntity entity) {
		LOG.debug("update " + entity);
		return entityManager.merge(entity);
	}

	@Override
	@Transactional(readOnly = true)
	public Object loadById(long id, Class<? extends AbstractPersistableEntity> persistentClass) {
		return entityManager.find(persistentClass, id);
	}

	@Override
	@Transactional(readOnly = true)
	public List findAll(Class<? extends AbstractPersistableEntity> persistentClass) {
		final CriteriaBuilder criteriaBuilder = getEntityManager().getCriteriaBuilder();
		final CriteriaQuery<? extends AbstractPersistableEntity> cqEmp = criteriaBuilder.createQuery(persistentClass);
		final Root entity = cqEmp.from(persistentClass);
		cqEmp.select(entity);
		final TypedQuery<? extends AbstractPersistableEntity> typedQuery = getEntityManager().createQuery(cqEmp);
		return typedQuery.getResultList();
	}

	@Override
	@Transactional(readOnly = true)
	public List<? extends AbstractPersistableEntity> findByRange(int first, int count, Class<? extends AbstractPersistableEntity> persistentClass) {
		final CriteriaBuilder cb = getBuilder();
		final CriteriaQuery<? extends AbstractPersistableEntity> criteria = cb.createQuery(persistentClass);
		final Root root = criteria.from(persistentClass);
		criteria.select(root);

		final TypedQuery<? extends AbstractPersistableEntity> typedQuery = getEntityManager().createQuery(criteria);
		typedQuery.setFirstResult(first);
		typedQuery.setMaxResults(count);
		return typedQuery.getResultList();
	}

	@Override
	@Transactional(readOnly = true)
	public int count(Class<? extends AbstractPersistableEntity> persistentClass) {
		final CriteriaBuilder cb = getBuilder();
		final CriteriaQuery<Long> cq = cb.createQuery(Long.class);
		final Root root = cq.from(persistentClass);
		cq.select(cb.count(root));
		return getEntityManager().createQuery(cq).getSingleResult().intValue();
	}

	@Override
	@Transactional
	public void remove(AbstractPersistableEntity entity, Class<? extends AbstractPersistableEntity> persistentClass) {
		if (entityManager.contains(entity)) {
			entityManager.remove(entity);
		} else {
			final Object attached = entityManager.find(persistentClass, entity.getId());
			entityManager.remove(attached);
		}
		entityManager.flush();
	}

	@Override
	@Transactional
	public void removeAll(List<? extends AbstractPersistableEntity> entities, Class<? extends AbstractPersistableEntity> persistentClass) {
		for (final AbstractPersistableEntity t : entities) {
			if (entityManager.contains(t)) {
				entityManager.remove(t);
			} else {
				final Object attached = entityManager.find(persistentClass, t.getId());
				entityManager.remove(attached);
			}
		}
		getEntityManager().flush();
	}

	@Override
	public void refresh(AbstractPersistableEntity entity) {
		entityManager.refresh(entity);
	}

	@Override
	@Transactional
	public void removeById(Long id, Class persistentClass) {
		entityManager.remove(entityManager.find(persistentClass, id));
	}

	public EntityManager getEntityManager() {
		return entityManager;
	}

	protected CriteriaBuilder getBuilder() {
		return getEntityManager().getCriteriaBuilder();
	}

	@Override
	public List search(AbstractPersistableEntity object, Class<? extends AbstractPersistableEntity> persistentClass) {

		Map<String, Object> criteria = new HashMap<String, Object>();

		getCriteria(object, object.getClass(), criteria);

		final CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
		final CriteriaQuery<? extends AbstractPersistableEntity> cq = cb.createQuery(persistentClass);
		final Root entity = cq.from(persistentClass);
		cq.select(entity);

		List<Predicate> predicates = new ArrayList<Predicate>();
		for (Entry<String, Object> entry : criteria.entrySet()) {
			String pattern = "%" + entry.getValue() + "%";
			try {
				Path<Object> path = entity.get(entry.getKey());
				predicates.add(cb.like((Expression)path, pattern));
			} catch (Exception e) {
				LOG.warn("error while getting attribute" + entry.getKey() + " for " + object, e);
			}

		}

		if (!predicates.isEmpty()) {
			cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()])));
		}

		return getEntityManager().createQuery(cq).getResultList();
	}

	private void getCriteria(Object object, Class<?> clazz, Map<String, Object> criteria) {

		List<FieldInfo> extractVisualFields = ClassUtils.extractVisualFields(clazz, false);
		for (FieldInfo fieldInfo : extractVisualFields) {
			if (!fieldInfo.getFieldAnotation().isTransient()) {
				Object result = ClassUtils.getValue(fieldInfo, object);
				if (result != null) {
					criteria.put(fieldInfo.getField().getName(), result);
				}
			}
		}
	}

	@Override
	public List searchExact(AbstractPersistableEntity object, Class<? extends AbstractPersistableEntity> persistentClass) {
		Map<String, Object> criteria = new HashMap<String, Object>();

		getCriteria(object, object.getClass(), criteria);

		final CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
		final CriteriaQuery<? extends AbstractPersistableEntity> cq = cb.createQuery(persistentClass);
		final Root entity = cq.from(persistentClass);
		cq.select(entity);

		List<Predicate> predicates = new ArrayList<Predicate>();
		for (Entry<String, Object> entry : criteria.entrySet()) {
			try {
				Path<Object> path = entity.get(entry.getKey());
				predicates.add(cb.equal(path, entry.getValue()));
			} catch (Throwable e) {
				LOG.error("error getting property " + entry.getKey() + " for " + object, e);
			}

		}

		if (!predicates.isEmpty()) {
			cq.where(cb.and(predicates.toArray(new Predicate[predicates.size()])));
		}

		return getEntityManager().createQuery(cq).getResultList();
	}

	@Override
	public List findByCriteria(Object value, String criteriaName, Class<? extends AbstractPersistableEntity> persistentClass) {

		final CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
		final CriteriaQuery<? extends AbstractPersistableEntity> cq = cb.createQuery(persistentClass);
		final Root entity = cq.from(persistentClass);
		cq.select(entity);

		cq.where(cb.equal(entity.get(criteriaName), value));

		return getEntityManager().createQuery(cq).getResultList();
	}

}
